
package jannex.mapping;

import jannex.JannexBuilder;
import jannex.JannexException;
import jannex.internal.ReflectionUtils;
import jannex.label.LabelSet;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

// TODO inheritance
//        Class<?> current = yourClass;
//        while(current.getSuperclass()!=null){ // we don't want to process Object.class
////        do something with current's fields
//        current = current.getSuperclass();
//        }
// TODO apply cases
public class MappingFactory {

    private final Map<String, Class<?>> typeMap;
    private final List<ExclusionStrategy> exclusionStrategies;
    private final boolean excludeFieldsWithoutColumnn;
    private final String defaultSchema;
    private final boolean excludeMethods;
    private final int excludeFieldModifiers;

    public MappingFactory(
            List<ExclusionStrategy> exclusionStrategies,
            boolean excludeFieldsWithoutColumnn,
            String defaultSchema,
            boolean excludeMethods,
            int excludeFieldModifiers, Map<String, Class<?>> typeMap) {
        this.exclusionStrategies = exclusionStrategies;
        this.excludeFieldsWithoutColumnn = excludeFieldsWithoutColumnn;
        this.defaultSchema = defaultSchema;
        this.excludeMethods = excludeMethods;
        this.excludeFieldModifiers = excludeFieldModifiers;
        this.typeMap = typeMap;
    }



//    public <E> MappedField<E> create(Class<E> elementClass, String fieldName, String  label, String  table,
//                                     String type, int modifiers) {
//        try {
//            Field field = elementClass.getDeclaredField(fieldName);
//            return create(elementClass, field, label, table, type, modifiers);
//        } catch (NoSuchFieldException e) {
//            throw new JannexException("Field not found '" + fieldName + "' in class " + elementClass.getName() + ".");
//        }
//    }

    public <E> MappedField<E> create(Class<E> elementClass, Field field, String label, String table,
                                     String sqlType, int modifiers) {
        String fieldName = field.getName();
        Class<?> type = field.getType();

        if (label == null) {
            label = fieldName;
        }

        Column column = field.getAnnotation(Column.class);
        if (column == null) {
            column = DefaultColumn.INSTANCE;
        }
        Entity entity = elementClass.getAnnotation(Entity.class);
        if (entity == null) {
            entity = DefaultEntity.INSTANCE;
        }

        if (!column.value().isEmpty()) {
            label = column.value();
        }

        if (modifiers < 0) {
            modifiers = 0;
            // Method/field named id or _id defaults to id, unless Column is used
            if ((fieldName.equalsIgnoreCase("id") || fieldName.equalsIgnoreCase("_id")) && column == null) {
                modifiers |= MappingModifiers.ID;
            }
            if (column.id()) {
                modifiers |= MappingModifiers.ID;
            }
            if (column.updatable()) {
                modifiers |= MappingModifiers.UPDATABLE;
            }
            if (column.selectable()) {
                modifiers |= MappingModifiers.SELECTABLE;
            }
            if (column.insertable()) {
                modifiers |= MappingModifiers.INSERTABLE;
            }
        }

        String capitalizedName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        FieldSetter<E> fieldSetter;
        FieldGetter<E> fieldGetter;

        Method getterMethod = getMethod(elementClass, "get" + capitalizedName);
        if (getterMethod == null && (type.equals(Boolean.class) || type.equals(Boolean.TYPE))) {
            getterMethod = getMethod(elementClass, "is" + capitalizedName);
        }

        Method setterMethod = getMethod(elementClass, "set" + capitalizedName);

        if (getterMethod == null) {
            fieldGetter = Fields.getterFromField(field);
        } else {
            fieldGetter = Fields.getterFromMethod(getterMethod);
        }
        if (setterMethod == null) {
            fieldSetter = Fields.setterFromField(field);
        } else {
            fieldSetter = Fields.setterFromMethod(setterMethod);
        }


        if (table == null) {
            table = "";
        }

        MappedField<?>[] children = null;
        if (!singleColumnField(type)) {
            children = createAll(field.getType());
            if (table.isEmpty()) {
                Join join = field.getAnnotation(Join.class);
                if (join != null && !join.value().isEmpty()) {
                    table = join.value();
                }
                if (table.isEmpty() && entity != null){
                    table = entity.value();
                }
                if (table.isEmpty()) {
                    table = field.getType().getSimpleName();
                }
            }
        } else {
            if (table.isEmpty()) {
                table = entity.value();
            }
            if (table.isEmpty()) {
                table = elementClass.getSimpleName();
            }
        }

        if (!column.type().isEmpty()) {
            if (!typeMap.containsKey(sqlType)) {
                throw new JannexException("Unknown type '" + sqlType + "'. Register type on "
                        + JannexBuilder.class.getSimpleName() + " to fix this.");
            } else if (!field.getType().isAssignableFrom(typeMap.get(sqlType))) {
                throw new JannexException("Unknown type '" + sqlType + "'. Register type on "
                        + JannexBuilder.class.getSimpleName() + " to fix this.");
            } else {
                type = typeMap.get(sqlType);
            }
        }

        return new MappedField<>(field, fieldSetter, fieldGetter, table, label, modifiers, type, children);
    }

    public <E> MappedField<E>[] createAll(Class<E> elementClass) {
        Field[] fields = elementClass.getDeclaredFields();
        List<MappedField<E>> fieldList = new ArrayList<>(fields.length);
        for (Field field : fields) {
            MappedField<E> mappedField = maybeCreateField(elementClass, field);
            if (mappedField != null) {
                fieldList.add(mappedField);
            }
        }
        @SuppressWarnings("unchecked")
        MappedField<E>[] mappedFields = fieldList.toArray((MappedField<E>[]) new MappedField[fieldList.size()]);
        return mappedFields;
    }

    public <E> Mapping<E> create(Class<E> elementClass) {

        Field[] fields = elementClass.getDeclaredFields();

        List<MappedField<E>> fieldList = new ArrayList<>(fields.length);

        for (Field field : fields) {
            MappedField<E> mappedField = maybeCreateField(elementClass, field);
            if (mappedField != null) {
                fieldList.add(mappedField);
            }
        }

        @SuppressWarnings("unchecked")
        MappedField<E>[] mappedFields = fieldList.toArray((MappedField<E>[]) new MappedField[fieldList.size()]);

        return create(elementClass, mappedFields, defaultSchema);
    }

    public <E> Mapping<E> create(Class<E> elementClass, MappedField<E>[] mappedFields, String schema) {

        Entity entity = elementClass.getAnnotation(Entity.class);
        if (entity == null) {
            entity = DefaultEntity.INSTANCE;
        }

        if (schema == null) {
            schema = defaultSchema;
        }
        if (!entity.schema().isEmpty()) {
            schema = entity.schema();
        }

        LabelSet.Builder orderBys = LabelSet.builder().addAll(entity.orderBy());
        LabelSet.Builder uniqueConstraints = LabelSet.builder().addAll(entity.uniqueConstraints());
        String id = null;

        for (MappedField<E> field : mappedFields) {
            int modifiers = field.getModifiers();
            String name = field.getLabel();
            if (MappingModifiers.isId(modifiers)) {
                id = name;
            }
        }

        String table = elementClass.getSimpleName();
        if (!entity.value().equals("")) {
            table = entity.value();
        }

        return new Mapping<>(
                elementClass,
                mappedFields,
                schema,
                table,
                buildArray(uniqueConstraints),
                buildArray(orderBys),
                id);
    }

    private <E> MappedField<E> maybeCreateField(Class<E> elementClass, Field field) {
        if (field.isSynthetic()) {
            return null;
        }
        for (ExclusionStrategy s : exclusionStrategies) {
            if (s.exclude(field)) {
                return null;
            }
        }
        int fieldModifiers = field.getModifiers();
        for (int mod = 1; mod < excludeFieldModifiers; mod <<= 2) {
            if ((excludeFieldModifiers & mod) != 0 && (fieldModifiers & mod) != 0) {
                return null;
            }
        }

        if (singleColumnField(field.getType())) {
            if (!excludeFieldsWithoutColumnn || field.getAnnotation(Column.class) != null) {
                return create(elementClass, field, null, null, null, -1);
            }
        } else {
            return create(elementClass, field, null, null, null, -1);
        }

        return null;
    }

    private String[] buildArray(LabelSet.Builder builder) {
        Set<String> set = builder.build();
        return set.toArray(new String[set.size()]);
    }

    private Method getMethod(Class<?> cls, String name) {

        if (excludeMethods) {
            return null;
        }
        try {
            Method declaredMethod = cls.getDeclaredMethod(name);
            if (declaredMethod == null) {
                return null;
            }
            if (!declaredMethod.isAccessible() || Modifier.isStatic(declaredMethod.getModifiers())) {
                return null;
            }
            return declaredMethod;
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private boolean singleColumnField(Class<?> type) {
        return ReflectionUtils.isPrimitivish(type) ||
                Collection.class.isAssignableFrom(type) ||
                Date.class.isAssignableFrom(type);
    }
}
